summaryrefslogtreecommitdiffstats
path: root/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt219
1 files changed, 219 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
index 569727b90..5b4bf2c9f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -7,20 +7,39 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.InstallableAdapter
import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.Installable
+import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.ui.main.MainActivity
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.FileUtil
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.File
+import java.math.BigInteger
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
class InstallableFragment : Fragment() {
private var _binding: FragmentInstallablesBinding? = null
@@ -56,6 +75,17 @@ class InstallableFragment : Fragment() {
binding.root.findNavController().popBackStack()
}
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ homeViewModel.openImportSaves.collect {
+ if (it) {
+ importSaves.launch(arrayOf("application/zip"))
+ homeViewModel.setOpenImportSaves(false)
+ }
+ }
+ }
+ }
+
val installables = listOf(
Installable(
R.string.user_data,
@@ -64,6 +94,43 @@ class InstallableFragment : Fragment() {
export = { mainActivity.exportUserData.launch("export.zip") }
),
Installable(
+ R.string.manage_save_data,
+ R.string.manage_save_data_description,
+ install = {
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.import_save_warning,
+ descriptionId = R.string.import_save_warning_description,
+ positiveAction = { homeViewModel.setOpenImportSaves(true) }
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ },
+ export = {
+ val oldSaveDataFolder = File(
+ "${DirectoryInitialization.userDirectory}/nand" +
+ NativeLibrary.getDefaultProfileSaveDataRoot(false)
+ )
+ val futureSaveDataFolder = File(
+ "${DirectoryInitialization.userDirectory}/nand" +
+ NativeLibrary.getDefaultProfileSaveDataRoot(true)
+ )
+ if (!oldSaveDataFolder.exists() && !futureSaveDataFolder.exists()) {
+ Toast.makeText(
+ YuzuApplication.appContext,
+ R.string.no_save_data_found,
+ Toast.LENGTH_SHORT
+ ).show()
+ return@Installable
+ } else {
+ exportSaves.launch(
+ "${getString(R.string.save_data)} " +
+ LocalDateTime.now().format(
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
+ )
+ )
+ }
+ }
+ ),
+ Installable(
R.string.install_game_content,
R.string.install_game_content_description,
install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }
@@ -121,4 +188,156 @@ class InstallableFragment : Fragment() {
windowInsets
}
+
+ private val importSaves =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ val inputZip = requireContext().contentResolver.openInputStream(result)
+ val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
+ cacheSaveDir.mkdir()
+
+ if (inputZip == null) {
+ Toast.makeText(
+ YuzuApplication.appContext,
+ getString(R.string.fatal_error),
+ Toast.LENGTH_LONG
+ ).show()
+ return@registerForActivityResult
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ requireActivity(),
+ R.string.save_files_importing,
+ false
+ ) {
+ try {
+ FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
+ val files = cacheSaveDir.listFiles()
+ var successfulImports = 0
+ var failedImports = 0
+ if (files != null) {
+ for (file in files) {
+ if (file.isDirectory) {
+ val baseSaveDir =
+ NativeLibrary.getSavePath(BigInteger(file.name, 16).toString())
+ if (baseSaveDir.isEmpty()) {
+ failedImports++
+ continue
+ }
+
+ val internalSaveFolder = File(
+ "${DirectoryInitialization.userDirectory}/nand$baseSaveDir"
+ )
+ internalSaveFolder.deleteRecursively()
+ internalSaveFolder.mkdir()
+ file.copyRecursively(target = internalSaveFolder, overwrite = true)
+ successfulImports++
+ }
+ }
+ }
+
+ withContext(Dispatchers.Main) {
+ if (successfulImports == 0) {
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.save_file_invalid_zip_structure,
+ descriptionId = R.string.save_file_invalid_zip_structure_description
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ return@withContext
+ }
+ val successString = if (failedImports > 0) {
+ """
+ ${
+ requireContext().resources.getQuantityString(
+ R.plurals.saves_import_success,
+ successfulImports,
+ successfulImports
+ )
+ }
+ ${
+ requireContext().resources.getQuantityString(
+ R.plurals.saves_import_failed,
+ failedImports,
+ failedImports
+ )
+ }
+ """
+ } else {
+ requireContext().resources.getQuantityString(
+ R.plurals.saves_import_success,
+ successfulImports,
+ successfulImports
+ )
+ }
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.import_complete,
+ descriptionString = successString
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ }
+
+ cacheSaveDir.deleteRecursively()
+ } catch (e: Exception) {
+ Toast.makeText(
+ YuzuApplication.appContext,
+ getString(R.string.fatal_error),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
+
+ private val exportSaves = registerForActivityResult(
+ ActivityResultContracts.CreateDocument("application/zip")
+ ) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ requireActivity(),
+ R.string.save_files_exporting,
+ false
+ ) {
+ val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
+ cacheSaveDir.mkdir()
+
+ val oldSaveDataFolder = File(
+ "${DirectoryInitialization.userDirectory}/nand" +
+ NativeLibrary.getDefaultProfileSaveDataRoot(false)
+ )
+ if (oldSaveDataFolder.exists()) {
+ oldSaveDataFolder.copyRecursively(cacheSaveDir)
+ }
+
+ val futureSaveDataFolder = File(
+ "${DirectoryInitialization.userDirectory}/nand" +
+ NativeLibrary.getDefaultProfileSaveDataRoot(true)
+ )
+ if (futureSaveDataFolder.exists()) {
+ futureSaveDataFolder.copyRecursively(cacheSaveDir)
+ }
+
+ val saveFilesTotal = cacheSaveDir.listFiles()?.size ?: 0
+ if (saveFilesTotal == 0) {
+ cacheSaveDir.deleteRecursively()
+ return@newInstance getString(R.string.no_save_data_found)
+ }
+
+ val zipResult = FileUtil.zipFromInternalStorage(
+ cacheSaveDir,
+ cacheSaveDir.path,
+ BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
+ )
+ cacheSaveDir.deleteRecursively()
+
+ return@newInstance when (zipResult) {
+ TaskState.Completed -> getString(R.string.export_success)
+ TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
+ }
+ }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
}